import { stripParensAndMaybeConvertToIife } from "@/wab/shared/core/exprs";
import { stampIgnoreError } from "@/wab/shared/error-handling";
import { maybeComputedFn } from "@/wab/shared/mobx-util";
import { $State } from "@plasmicapp/react-web";

export const ENABLED_GLOBALS = new Set([
  "Array",
  "Boolean",
  "Date",
  "Infinity",
  "Intl",
  "JSON",
  "Map",
  "Math",
  "NaN",
  "Number",
  "Object",
  "Promise",
  "Blob",
  "ReferenceError",
  "RegExp",
  "Set",
  "String",
  "TypeError",
  "alert",
  "clearInterval",
  "clearTimeout",
  "parseInt",
  "confirm",
  "console",
  "localStorage",
  "prompt",
  "sessionStorage",
  "setInterval",
  "setTimeout",
  "undefined",
  "window",
  "fetch",
]);

/**
 * Returns a function that takes in a sandbox scope object (which contains
 * only variables accessible to the code), and an optional "this" object
 * (which defaults to empty object), and evaluates `src` in that context.
 *
 * Based on
 * https://blog.risingstack.com/writing-a-javascript-framework-sandboxed-code-evaluation/
 * with additions on binding `this`.
 *
 * Note that this is NOT SECURE; it simply prevents unintentionally leaking
 * in globals.  Globals are very much still ACCESSIBLE!  For example,
 * you can get to the global Object just by evaluating `({}).constructor`.
 */
export function compileCodeExpr(
  src: string,
  currGlobalThis: typeof globalThis = globalThis
) {
  return _compileCodeExpr(src, currGlobalThis);
}

const _compileCodeExpr = maybeComputedFn(function _compileCodeExpr(
  src: string,
  currGlobalThis: typeof globalThis
) {
  const makeFunction = () => {
    try {
      return new currGlobalThis.Function(
        "sandbox",
        `
        with (sandbox) {
          return (
            ${stripParensAndMaybeConvertToIife(src)}
          );
        }`
      );
    } catch (err) {
      // Syntax error
      console.error(
        `Error constructing evaluation function for code \`${src}\`: `,
        err
      );
      throw err;
    }
  };

  const code = makeFunction();

  return function (sandbox: object, thisObj?: object) {
    const sandboxProxy = new Proxy(sandbox, {
      has: (_: object, key: PropertyKey) => {
        if (typeof key === "string" && ENABLED_GLOBALS.has(key)) {
          return false;
        }
        return true;
      },
      get: (target: object, key: PropertyKey) => {
        if (key === Symbol.unscopables) {
          return undefined;
        } else if (key === "globalThis") {
          return currGlobalThis;
        } else if (!(key in target)) {
          throw stampIgnoreError(
            new ReferenceError(`${key.toString()} is not defined`)
          );
        } else {
          return target[key];
        }
      },
    });

    try {
      return code.bind(thisObj ?? {})(sandboxProxy);
    } catch (err) {
      console.error(`Error evaluating custom code \`${src}\`:`, err);
      throw err;
    }
  };
});

export function evalExprInSandbox(
  code: string,
  sandbox: object,
  thisObj?: object,
  currGlobalThis: typeof globalThis = globalThis
) {
  return compileCodeExpr(code, currGlobalThis)(sandbox, thisObj);
}

export interface CanvasEnv {
  /**
   * Data context (generated by @plasmicapp/host DataProvider).
   */
  $ctx: Record<string, any>;
  /**
   * Current component props.
   */
  $props: Record<string, any>;
  /**
   * Current component states.
   */
  $state: $State;
  /**
   * Current query data.
   */
  $queries: Record<string, any>;
  /**
   * Mapping from element uuid to its ref
   */
  $refs: Record<string, any>;
  /**
   * Registered custom functions
   */
  $$: Record<string, Function | Record<string, Function>>;
  /**
   * Data tokens
   */
  $dataTokens?: Record<string, any>;
  /**
   * Other variables (set by dataRep).
   */
  [key: string]: any;
}

export function evalCodeWithEnv(
  code: string,
  data: Record<string, any>,
  currGlobalThis: typeof globalThis = globalThis
) {
  try {
    return currGlobalThis.JSON.parse(code);
  } catch {
    return evalExprInSandbox(code, data, undefined, currGlobalThis);
  }
}

export interface TryEvalExprResult {
  val: any | undefined;
  err: Error | undefined;
}

export function tryEvalExpr(
  code: string,
  data: Record<string, any>,
  currGlobalThis: typeof globalThis = globalThis
): TryEvalExprResult {
  try {
    return { val: evalCodeWithEnv(code, data, currGlobalThis), err: undefined };
  } catch (e) {
    return { val: undefined, err: e };
  }
}
